﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using ALDIOrganizer;

namespace Generator
{
    /* This static class is responsible for calculating penalties
     * which are used by the Engine class. */


    public static partial class Penalties
    {


        #region run-Time customizable parameters

        public static Parameters Parameters { get; private set; }

        public static void SetParameters(Parameters p)
        {
            Parameters = p;
            
        }
        #endregion


        #region auxiliary private methods (for bit array calculations mostly)
        static bool[] ByteToBoolArr(byte b)
        {
            if (b > 127)
            {
                throw new ArgumentException("EnginePenalties.ByteToBoolArr - b can't be > 127! we only return 7 bits");
            }
            var result = new bool[7];
            for (int i = 6; i >= 0; i--)
            {
                result[i] = ((b & 1) == 1);
                b = (byte)(b >> 1);
            }
            return result;                
        }
                        
        internal static byte preferredNumberOfWorkingDays(int contractHours)
        {
            var x = contractHours / 15.0 + 1.8;
            if (x > 7)
            {
                x = 7.0;
            }
            return (byte)x;
        }
        
        internal static double overDaysPenalty(byte actualDays, byte supposedDays)
        {
            double result = 0.0;

            if (actualDays <= supposedDays) return result;

            var excess = actualDays - supposedDays;
            
            for (int i = 0; i < excess; i++)
            {
                result += Penalties.overdaysPenalty_fractionalWeightBASE;
                if (i > 0)
                {
                    result += (Penalties.overdaysPenalty_fractionalWeightEXTRA * i);
                }
            }

            if (actualDays == 6)
            {
                result *= Penalties.overdaysPenalty_fractionalWeightSIXDAYS;
            }
            if (actualDays == 7)
            {
                result *= Penalties.overdaysPenalty_fractionalWeightWHOLEWEEK;
            }            
            return result;
        }
        #endregion

        #region dictionaries, or "tables" as _howManyWeHave call them

        public static bool dictionariesReady { get; private set; }

        static Dictionary<int, double> _powTable;
        public static Dictionary<int, double> powTable
        {
            get { return _powTable; }
        }

        // [below] = bullshit

        /* arrays of dictionaries?? what are they
         * well they're supposed to hold values for calculating penalties
         * associated with the DAYS the _employee works.
         * the key (byte) is a 7-bit array, in which we convey the information On
         * what bits the _employee works.
         * for example, if someone works date, wednesday-friday, Sunday,
         * then this Value will be (in binary system) 1011101, or 103.
         * so that'pShift the key, and the int Value?
         * the int Value returns the penalty for this particular pattern.
         * for example, in this 1011101 case, those two bits off that he has
         * are split apart.
         * his working bits form three "islands", instead of two (such as
         * 1110011 or 1100111) or even one.
         * this will be penalized, if the user sets Parameters.avoidInARow
         * to true.
         * the same with weekends. if Parameters.SplitWeekendsFairly is set to true,
         * then the engine will penalize every case where the _employee has to work
         * BOTH saturday and Sunday.
         * in other words - every solution which ends with ...11
         * 
         * but why do we still have ARRAYS of those dictionaries?
         * well, this is done because certain emps_from_database may NOT be available to work
         * all week round (and it shows just how much I care about details ;> )
         * for example, someone can still be On _holidays On Monday and Tuesday,
         * and not be able to work On Thursday, so theoretically he'pShift only able to work
         * 4 bits a week.
         * in this case, assigning him both saturday and Sunday may be harder to avoid,
         * and will be LESS PENALIZED than for someone who is available all week.
         * When somebody [...] */
        static Dictionary<byte, int>[] _tblFullShiftsInARowPenalties;
        public static Dictionary<byte, int>[] tblFullShiftsInARowPenalties
        {
            get
            {
                return _tblFullShiftsInARowPenalties;
            }
        }
        static Dictionary<byte, int>[] _tblWeekendsPenalties;
        public static Dictionary<byte, int>[] tblWeekendsPenalties
        {
            get
            {
                return _tblWeekendsPenalties;
            }
        }

        /* in this case, the index of the array indicates the number of bits the guy is SUPPOSED
         * to work (based On his _contract hours) */
        static Dictionary<byte, int>[] tblNumberOfWorkingDaysPenalties;
        

        public static void PreparePenaltyCharts()
        {
            
            if (dictionariesReady) return; // we don't do the same job twice
            
            // creating tables
            #region _powTable

            // preparing tables to avoid
            _powTable = new Dictionary<int, double>();

            // from 80 hours of undertime, to 80 hours of overtime
            for (int i = -320; i < 0; i++)
            {
                if (i > (0 - (Penalties.undertimeTolerance * 4)))
                {
                    _powTable.Add(i, Math.Pow(Math.Abs(i), Penalties.tolerantUndertimeExponent));
                }
                else
                {
                    _powTable.Add(i, Math.Pow(Math.Abs(i), Penalties.undertimeExponent));
                }
            }
            for (int i = 0; i < 321; i++)
            {
                if (i > (Penalties.overtimeTolerance * 4))
                {
                    _powTable.Add(i, Math.Pow(i, Penalties.overtimeExponent));
                }
                else
                {
                    _powTable.Add(i, Math.Pow(i, Penalties.tolerantOvertimeExponent));
                }
            }

            #endregion

            #region penalties

            /* penalty for full Shifts in a row - index stands for the maximum set.
             * so, if - for example - somebody works 0111000 (in terms of full Shifts;
             * Open till Close On Tuesday, Wednesday and Thursday)
             * then... 0111000 converted to decimal is 48
             * tblFullShiftsInARowPenalties[4][48] will be 0
             * tblFullShiftsInARowPenalties[3][48] will be 0
             * tblFullShiftsInARowPenalties[2][48] will be greater than zero, because
             * 3 Shifts in a row is more than 2 */
            _tblFullShiftsInARowPenalties = new Dictionary<byte, int>[8];

            /* an array which returns the penalty for the number of bits the _employee works.
             * how does it work?
             * the index represents how many bits the _employee is SUPPOSED to work.
             * this is calculated basing On his _contract hours. */
            tblNumberOfWorkingDaysPenalties = new Dictionary<byte, int>[8];

            /* here the index means the number of bits the _employee is AVAILABLE to work.
             * it can be 7 bits, if he has no commitments (and, usually it will be just so). */
            _tblWeekendsPenalties = new Dictionary<byte, int>[8];

            _tblFullShiftsInARowPenalties = new Dictionary<byte, int>[8];

            /* ^ so, do not confuse the tblNumberOfWorkingDaysPenalties index
             * with the _tblWeekendsPenalties index!
             * 
             * let'pShift suppose somebody is On a 30 hours _contract, and cannot work On Wednesdays,
             * because he'pShift got college all Day.
             * number of bits he'pShift SUPPOSED to work? 3. (calculated by the [(30/15 + 1.8)]=3.8 formula).
             * number of bits he'pShift AVAILABLE to work? 6. (all bits minus Wednesday).
             * 
             * so, imagine the solution we've got for him sets his working week as "1101011" (= 107)
             * he works all bits, except for Wednesday and Friday.
             * 
             * where will we look his penalties up?
             * tblNumberOfWorkingDaysPenalties[3]<-array index [107] <- dictionary key
             * and
             * _tblWeekendsPenalties[6]<-array index [107] <- dictionary key */
            
            
            int weekends = 0; // ...to _tblWeekendsPenalties
            int numberofworkingdays; // ...and to tblNumberOfWorkingDaysPenalties
            
            for (int noOfDays = 0; noOfDays <= 7; noOfDays++)
            {
                #region initializing dictionaries in the array                
                _tblWeekendsPenalties[noOfDays] = new Dictionary<byte, int>();
                _tblFullShiftsInARowPenalties[noOfDays] = new Dictionary<byte, int>();
                tblNumberOfWorkingDaysPenalties[noOfDays] = new Dictionary<byte, int>();
                #endregion

                // for every possible pattern, from 0000000 to 1111111
                for (byte b = 0; b < 128; b++)
                {
                    var bitArr = ByteToBoolArr(b);
                    var howManyDaysWorks = bitArr.NumberOfDays();
                    var inARow = bitArr.HowManyInARow();
                                        
                    /* noOfDays in this case means: the maximum set by Parameters.MaximumFullShiftsInARow.
                     * if we do not exceed this maximum, there is no penalty. */
                    if (inARow > noOfDays)
                    {
                        tblFullShiftsInARowPenalties[noOfDays].Add(
                            b,
                            Penalties.fullshiftPenalties[inARow - noOfDays]);
                    }
                    else
                    {
                        tblFullShiftsInARowPenalties[noOfDays].Add(
                            b,
                            0);
                    }
                    /* in case of weekend penalties, the array index means "how many bits the _employee is
                     * available for (this week).
                     * hence we ignore howManyDaysWorks values greater than noOfDays, because if the _employee
                     * is - for example - only able to do 5 bits, then it makes no sense to calculate "and what would
                     * happen if he did 6, or 7", because it is by definition prohibited and will never happen. */                    
                    if (howManyDaysWorks <= noOfDays)
                    {                        
                        #region weekend penalty
                        weekends = 0;
                        if (bitArr.DoesWorkWholeWeekend())
                        {
                            // 
                            /* how avoidable is it? if the _employee can work only 2 bits, then we won't penalize
                             * his working On the weekend at all, because if he works saturday and Sunday, these
                             * must have been the only bits he was available to work. "tough luck" it'pShift not our fault */
                            double howAvoidable = (double)(noOfDays - 2) / (double)7;

                            if (howAvoidable < 0) howAvoidable = 0.0;                                
                                                        
                            weekends = (int)((double)Penalties.weekendPenaltyWeight* howAvoidable);
                            
                        }
                        #endregion
                    }

                    #region number of working bits penalty
                    
                    /* in this case, noOfDays indicates how many bits the _employee was
                     * SUPPOSED to work (calculated (not here, but in the caller) basing
                     * On his _contract hours.
                     * we only penalize assigning MORE hours than he is suppoed to work. */

                    numberofworkingdays = (int)((double)Penalties.overdaysPenaltyWeight * overDaysPenalty(howManyDaysWorks, (byte)noOfDays));
                    #endregion
                    
                    _tblWeekendsPenalties[noOfDays].Add(b, weekends);
                    tblNumberOfWorkingDaysPenalties[noOfDays].Add(b, numberofworkingdays);
                }
            }

            #endregion

            dictionariesReady = true;
         
        }
        #endregion
        
        #region actual penalties
        // hours are really quarters. _contract = full hours
        public static double penaltyHours(int hours, int contract)
        {

            /* we don't perform any check On the Penalties.parameters,
             * because this penalty *always* applies (you cannot switch it off!) */
            int key = hours - (contract * 4);
            
            return Penalties.powTable[key];
        }

        public static int difPenaltyHours(int hours, int contract, int shiftLength)
        {
            /* as above ^
             * - we don't perform any check On the Penalties.parameters,
             * because this penalty *always* applies (you cannot switch it off!) */
            var result = penaltyHours(hours+shiftLength, contract) - penaltyHours(hours, contract);
            return (short)result;
        }

        public static int penaltyDays(bool[] days, short howManySupposedTo)
        {
            if (!Parameters.PenalizeOverdays)
            {
                return 0;
            }
            if (days.Length != 7)
            {
                throw new ArgumentException(
                    "EnginePenalties.penaltyDays: days is supposed to be bool[7]! wrong array length");
            }
            if (!dictionariesReady)
            {
                PreparePenaltyCharts();
            }
            var b = days.ToByte();
            return tblNumberOfWorkingDaysPenalties[howManySupposedTo][b];
        }

        public static int difPenaltyDays(bool[] days, byte whichDayWouldItBe, short howManySupposedTo)
        {
            if (!Parameters.PenalizeOverdays)
            {
                // 
                return 0;
            }
            if (days[whichDayWouldItBe])
                return Penalties.revisitPenalty;

            int x = Penalties.penaltyDays(days, howManySupposedTo);
            int y = Penalties.penaltyDays(days.WithBitOn(whichDayWouldItBe), howManySupposedTo);

            return (y - x);
        }
        
        public static int penaltyWeekend(bool[] days, short howManyAvailable)
        {
            if (!Parameters.SplitWeekendsFairly)
            {
                // 
                return 0;
            }
            if (days.Length != 7)
                throw new ArgumentException("EnginePenalties.penaltyWeekend: days is supposed to be bool[7]! wrong array length");
            if (!dictionariesReady)
                PreparePenaltyCharts();

            byte b = days.ToByte();
            
            return tblWeekendsPenalties[howManyAvailable][b];            
        }

        public static int difPenaltyWeekend(bool[] days, short howManyAvailable, byte whichDayWouldItBe)
        {
            if (!Parameters.SplitWeekendsFairly)
            {
                
                return 0;
            }
            
            int x = penaltyWeekend(days, howManyAvailable);
            int y = penaltyWeekend(days.WithBitOn(whichDayWouldItBe), howManyAvailable);            
                        
            return (y - x);
        }

        public static int penaltyFullShiftsInARow(bool[] fullDays)
        {
            if (Parameters.MaximumFullShiftsInARow == 0)
                return 0;
            return tblFullShiftsInARowPenalties[Parameters.MaximumFullShiftsInARow][fullDays.ToByte()];
        }

        public static int difPenaltyFullShiftsInArow(bool[] fullDays, byte whichDayWouldBeAdded)
        {
            var b2 = ByteToBoolArr(fullDays.WithBitOn(whichDayWouldBeAdded).ToByte());

            return penaltyFullShiftsInARow(b2) - penaltyFullShiftsInARow(fullDays);            
        }

       


        // calculates the "key flow" rate basing On the contents of above "openers" and "closers" lists
        public static int ComputeKeyFlow(
            bool[,] openers,
            bool[,] closers,
            Candidate[] pCandidates,
            bool considerPreviousWeek,
            bool pIsReportRequested,
            out KeyFlow pReport)
        {
            var result = 0;
            if (!Parameters.KeyFlowOptimizationOn)
            {
                pReport = null;
                return result;
            }
            
            int howManyIDs = openers.GetUpperBound(1); // retrieves the height of the openers/closers arrays.

            pReport = pIsReportRequested ? new KeyFlow() : null;

            for (int day = 0; day < 7; day++)
            {
                // if Day > date, or it IS date, but we do consider Monday
                if ((day <= 0) &&
                    ((day != 0) || !considerPreviousWeek))
                {
                    continue;
                }
                bool foundTheSamePerson = false;
                bool checkedAllPeople = false;
                int index = 0;
                    
                // we stop as soon as we found the same person OR got to the Finish of the array
                while (!foundTheSamePerson && 
                       !checkedAllPeople)
                {
                    var dayToCompare = day - 1;
                    if(dayToCompare < 0)
                        dayToCompare = 6;
                    if (openers[day, index])
                    {
                        if ((day >= 0) & (closers[dayToCompare, index])
                            | (day == 0) & (pCandidates[index].ClosesOnPreviousSunday))
                        {                            
                            foundTheSamePerson = true;
                            if (pIsReportRequested)
                            {
                                pReport.Days[day].AddOpener(index);
                                pReport.Days[dayToCompare].AddCloser(index);
                            }
                        }
                    }

                    if (index == howManyIDs)
                    {
                        checkedAllPeople = true;
                    }
                    index++;
                }

                if (foundTheSamePerson)
                {
                    result++;
                }
            }

            if (pIsReportRequested)
            {
                pReport.Maximum = considerPreviousWeek ? (byte)7 : (byte)6;
                pReport.Value = (byte)result;
                pReport.IncludePrevWeek = considerPreviousWeek;
            }

            return result;
        }
        
        #endregion



    }
}
